package code

import (
	"bytes"
	"encoding/binary"
	"fmt"
)

// 所有指令的集合，包括操作码、数据在常量池中的索引
type Instructions []byte

// 指令集的字符串输出
// 只要定义了该方法，就能在调用 fmt.Print(Instruction) 的时候自动触发该方法
func (ins Instructions) String() string {
	// 输出规则：每行起始位置展示字节位置索引 然后是单字节的操作码的值 然后是双字节的数据索引的值
	var out bytes.Buffer
	i := 0
	for i < len(ins) {
		op := ins[i]
		def, err := Lookup(op)
		if err != nil {
			fmt.Fprintf(&out, "ERROR: %s\n", err)
			continue
		}

		operands, read := ReadOperands(def, ins[i+1:])
		fmt.Fprintf(&out, "%04d %s\n", i, fmtOpcodeInstruction(def, operands))
		i += read + 1
	}

	return out.String()
}

// operands 参数表示的是常量池中的参数索引
func fmtOpcodeInstruction(def *Definition, operands []int) string {
	operandCount := len(def.OperandWidths)
	if len(operands) != operandCount {
		return fmt.Sprintf("ERROR: operand len %d does not match defined %d\n", len(operands), operandCount)
	}
	switch operandCount {
	case 0:
		return def.Name
	case 1:
		return fmt.Sprintf("%s %d", def.Name, operands[0])
	}

	return fmt.Sprintf("ERROR: unhandled operandCount for %s\n", def.Name)
}

// - 操作码
// 一个字节，比如用来表示 PUSH ADD 之类的
type Opcode byte

// -- 具体的操作码，不同的操作码会告知编译器不同的处理行为
const (
	// 常量池操作码：编译器碰到该操作码后会继续找一个操作数；该操作数之前已经分配在常量池之中了，用一个索引数字来代替了。
	OpConstant Opcode = iota // 常量池指针操作码 - 该操作码会尾随一个操作数，该操作数表示了常量池中的对象的索引
	// 中缀运算符的操作码
	OpAdd   // 相加操作码 - 该操作码从栈中弹出两个对象执行相加操作
	OpPop   // 清理操作码 - 该操作码将栈顶元素弹出，相当于清理掉不需要的对象
	OpSub   // 减
	OpMul   // 乘
	OpDiv   // 除
	OpTrue  // 布尔-真
	OpFalse // 布尔-假
	OpEqual
	OpNotEqual
	OpGreaterThan
	// 前缀运算符的操作码
	OpMinus // 负数前缀 -
	OpBang  // 取反前缀 !
	// 跳转操作码
	OpJumpNotTruthy
	OpJump
	// 特殊操作码
	OpNull // 此时表示有一个空对象
	// 变量操作码
	OpSetGlobal
	OpGetGlobal
	// 符合类型
	OpArray // 数组
	OpHash  // 哈希
	OpIndex // 索引
	// 函数相关
	OpCall        // 调用
	OpReturnValue // 返回函数执行的结果，其实就是压栈
	OpReturn      // 空函数体没有结果
)

// -- 操作码的定义，用来调试使用
type Definition struct {
	Name          string // 操作码的名称
	OperandWidths []int  // 操作码的每个操作数占用的字节大小；切片的长度表示操作数的个数，每个切片元素的值表示字节数
}

var definitions = map[Opcode]*Definition{
	OpConstant: {"OpConstant", []int{2}}, // 常量池操作码，名字叫做OpConstant，操作数只有一个，该操作数占据两个字节宽度。
	OpAdd:      {"OpAdd", []int{}},       // 相加操作码
	OpPop:      {"OpPop", []int{}},       // 清理操作码
	OpSub:      {"OpSub", []int{}},       // 相减操作码
	OpMul:      {"OpMul", []int{}},       // 相乘操作码
	OpDiv:      {"OpDiv", []int{}},       // 相除操作码
	OpTrue:     {"OpTrue", []int{}},      // 实际上布尔值也可以想 OpConstant 那样通过 操作码+操作数 的方式来取指，但是这样浪费空间，还不如直接定义一个特殊的操作码
	OpFalse:    {"OpFalse", []int{}},     //
	// 比较运算符 - 不需要操作数，通过比较栈顶来完成工作
	OpEqual:       {"OpEqual", []int{}},
	OpNotEqual:    {"OpNotEqual", []int{}},
	OpGreaterThan: {"OpGreaterThan", []int{}}, // 此处的大于号操作符可以将小于号操作符的功能也囊括进来，因为编译器只要进行比较数的交换即可。
	OpMinus:       {"OpMinus", []int{}},
	OpBang:        {"OpBang", []int{}},
	// 跳转操作码 - 操作数表示偏移量，使用16位宽度
	OpJumpNotTruthy: {"OpJumpNotTruthy", []int{2}}, // 当if条件假时跳转，操作数为指令字节码的绝对字节偏移量
	OpJump:          {"OpJump", []int{2}},          // 当if条件为真，且已经执行完了真值的语句块之后，跳转到假值得语句块之后
	OpNull:          {"OpNull", []int{}},
	// 变量操作码
	OpSetGlobal: {"OpSetGlobal", []int{2}},
	OpGetGlobal: {"OpGetGlobal", []int{2}},
	// 复合类型
	OpArray: {"OpArray", []int{2}}, // 数组及其可操作数
	OpHash:  {"OpHash", []int{2}},
	OpIndex: {"OpIndex", []int{}},
	// 函数
	OpCall:        {"OpCall", []int{}}, // 直接弹栈执行函数体
	OpReturnValue: {"OpReturnValue", []int{}},
	OpReturn:      {"OpReturn", []int{}},
}

// 查找某一个具体的操作码的定义
func Lookup(op byte) (*Definition, error) {
	def, ok := definitions[Opcode(op)]
	if !ok {
		return nil, fmt.Errorf("opcode %d undefined", op)
	}
	return def, nil
}

// - method
// Make 方法将操作码和操作数构造成一个连续的内存字节码指令集合
// operands表示操作数，是在常量池中的索引，注意数字在当前的指令集合中以大端序来存储。
func Make(op Opcode, operands ...int) Instructions {
	def, ok := definitions[op]
	if !ok {
		return []byte{} // 没有定义的话响应为空
	}

	// 计算指令集合占据的内存大小
	instructionLen := 1
	for _, width := range def.OperandWidths {
		instructionLen += width
	}
	instruction := make([]byte, instructionLen)
	instruction[0] = byte(op)

	// 将操作数逐个写入到指令集合中，注意数字是以大端序存储的，所以写入的时候要转化一下字节顺序，通常x86的机器是小端序的。
	offset := 1
	for i, o := range operands {
		// 第i个操作数占据的内存宽度
		width := def.OperandWidths[i]
		switch width {
		case 2:
			// 如果是2个字节的宽度，则将该数字先转为uint16，然后通过标准库转为大端序写入切片中
			binary.BigEndian.PutUint16(instruction[offset:], uint16(o))
		}
		offset += width
	}

	return instruction
}

// 根据指令字节码的定义规范将操作数索引解码出来
// 返回值：操作数索引切片 和 总共有多少个
// Make的反操作
func ReadOperands(def *Definition, ins Instructions) ([]int, int) {
	widths := def.OperandWidths
	operands := make([]int, len(widths))

	offset := 0

	for i, width := range widths {
		switch width {
		// 目前字节码中只有2字节表示的操作数索引
		case 2:
			val := binary.BigEndian.Uint16(ins[offset:]) // 这个 binary.BigEndian.Uint16实际上就是读取两个字节，所以此处不需要指定 [offset:offset+2]
			operands[i] = int(val)
		}

		offset += width
	}

	return operands, offset
}

func ReadUint16(data []byte, offset int) uint16 {
	return binary.BigEndian.Uint16(data[offset:])
}
